package cn.edu.hbpu.service.impl;

import cn.edu.hbpu.common.R;
import cn.edu.hbpu.dto.LoginUser;
import cn.edu.hbpu.dto.UserDto;
import cn.edu.hbpu.entity.User;
import cn.edu.hbpu.handler.CustomException;
import cn.edu.hbpu.mapper.UserMapper;
import cn.edu.hbpu.service.UserService;
import cn.edu.hbpu.utils.JwtUtils;
import cn.edu.hbpu.utils.RedisCache;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.HttpStatus;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.code.kaptcha.Producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static cn.edu.hbpu.common.Constants.CODE_EXPIRED_TIME;
import static cn.edu.hbpu.common.Constants.LOGIN_EXPIRED_TIME;

/**
 * @author lcy
 * @date 2022/8/4 16:00
 * @description
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    /**
     * userMapper
     */
    @Autowired
    private UserMapper userMapper;

    /**
     * 密码加密对象
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 引入验证码生成器对象
     */
    @Autowired
    private Producer producer;

    /**
     * RedisCache：redis工具类
     */
    @Autowired
    private RedisCache redisCache;

    /**
     * 1. 获取AuthenticationManager对象
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    private void verifyCode(UserDto user) {
        // 校验验证码
        // codeKey
        String codeKey = user.getCodeKey();
        // code
        String code = user.getCode();

        // 获取前端传来的codeKey
        if (Objects.isNull(codeKey)) {
            throw new CustomException("请传入codeKey");
        }
        if (Objects.isNull(code)) {
            throw new CustomException("请传入验证码");
        }
        // 根据codeKey从数据库中获取code值
        String codeFromRedis = redisCache.getCacheObject("user:code:" + codeKey);
        // 获取不到说明该键值对已经过期
        if (Objects.isNull(codeFromRedis)) {
            throw new CustomException("验证码已过期");
        }
        // 如果两边验证码不同，说明传入的验证码错误
        if (!code.equalsIgnoreCase(codeFromRedis)) {
            // 验证码错误，则删除此时codeKey在redis中的验证码
            redisCache.deleteObject("user:code:" + user.getCodeKey());
            throw new CustomException("请输入正确的验证码");
        }
    }

    @Override
    public R<Map<Object, Object>> login(UserDto user) {
        // 校验验证码
        verifyCode(user);
        // 能继续往下走，说明以上步骤正确，且用户输入的验证码也正确

        // 2. 调用该对象的authenticate进行身份验证。该方法最终会调用实现了UserDetailsService的接口的loadUserByUsername方法
        // 2.1 创建UsernamePasswordAuthenticationToken对象，并传入用户名和密码。
        Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword(), null);
        // 2.2 调用authenticate方法后会得到封装了loadUserByUsername方法返回值的Authentication对象
        Authentication authenticate = authenticationManager.authenticate(authentication);
        // 3. 判断用户是否认证成功
        if (Objects.isNull(authenticate)) {
            throw new CustomException("登录失败");
        }
        // 4. 认证成功获取用户信息
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 4.1 查看账号是否可用
        int status = loginUser.getUser().getStatus();
        if (status <= 0) {
            throw new CustomException("账号已锁定，请联系管理员");
        }
        // 4.2 获取用户id
        String userId = loginUser.getUser().getUserId();
        // 4.3 将用户id加密为token令牌
        String jwt = JwtUtils.createJwt(userId);
        // 4.4 登录成功后，删除验证码
        redisCache.deleteObject("user:code:" + user.getCodeKey());

        // 5. 根据用户id将用户信息写入redis
        redisCache.setCacheObject("user:loginedUser:" + userId, loginUser, LOGIN_EXPIRED_TIME, TimeUnit.HOURS);
        return new R<>(HttpStatus.HTTP_OK, "登录成功", MapUtil.builder().put("Authorization", jwt).build());
    }

    @Override
    public R<Map<Object, Object>> register(UserDto user) {
        // 1. 校验验证码
        verifyCode(user);
        // 2. 比较两次输入的密码是否一样
        String userName = user.getUserName();
        String password = user.getPassword();
        String confirmPassword = user.getConfirmPassword();
        if (Objects.isNull(userName) || Objects.isNull(password) || Objects.isNull(confirmPassword)) {
            throw new CustomException("请输入用户名和密码");
        }
        if (!password.equals(confirmPassword)) {
            throw new CustomException("两次输入的密码不一致");
        }
        // 将密码进行加密
        String encryptedPassword = passwordEncoder.encode(password);
        // 保存加密后的密码到数据库
        user.setPassword(encryptedPassword);
        // 随机设置用户昵称
        String nickName = "zyg" + RandomUtil.randomNumbers(8);
        user.setNickName(nickName);
        // 设置创建用户默认为男生
        user.setGender(1);
        // 设置账号默认可用
        user.setStatus(1);

        // 3. 将用户数据写入数据库
        userMapper.insert(user);
        // 3.1 写入失败，用户名重复，被全局异常捕获
        // 3.2 写入成功
        // 4. 根据用户名查询出新注册用户的信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, userName);
        User registeredUser = userMapper.selectOne(queryWrapper);

        // 5. 将userId加密为token令牌
        String userId = registeredUser.getUserId().toString();
        String token = JwtUtils.createJwt(userId);

        // 6. 将用户信息封装成LoginUser再写入redis缓存
        LoginUser loginUser = new LoginUser(registeredUser);
        redisCache.setCacheObject("user:loginedUser:" + userId, loginUser, LOGIN_EXPIRED_TIME, TimeUnit.HOURS);

        return new R<>(HttpStatus.HTTP_OK, "注册成功", MapUtil.builder()
                .put("Authorization", token).build());
    }

    @Override
    public R<Map<Object, Object>> generateCode() throws IOException {
        // 验证码存入redis中的键值
        String codeKey = UUID.randomUUID().toString();
        // 验证码值
        String code = producer.createText();
        log.info("code = " + code);
        // 根据事先配置的规则创建验证码图片
        BufferedImage image = producer.createImage(code);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        // 将生成的图片写入 outputStream 中
        ImageIO.write(image, "jpg", outputStream);
        // 将图片转为BASE64格式并返回
        String str = "data:image/jpeg;base64,";
        String base64Image = str + Base64Utils.encodeToString(outputStream.toByteArray());

        // 将验证码值存入redis并设置过期时间为 60s
        redisCache.setCacheObject("user:code:" + codeKey, code, CODE_EXPIRED_TIME, TimeUnit.SECONDS);

        // 返回验证码
        return new R<>(HttpStatus.HTTP_OK, MapUtil.builder()
                .put("codeKey", codeKey)
                .put("base64Img", base64Image)
                .build());
    }

    @Override
    public R<Map<Object, Object>> logout() {
        // 1. 因为经过了校验过滤器，所以可以从SecurityContextHolder取出 LoginUser
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 因为 /user/logout已经存放在白名单，所以 authentication.getPrincipal() 的默认值为anonymousUser
        if ("anonymousUser".equals(authentication.getPrincipal())) {
            throw new CustomException("请传入token令牌");
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

        System.out.println(loginUser);
        // 2. 将用户信息从redis中清除
        String userId = loginUser.getUser().getUserId();
        redisCache.deleteObject("user:loginedUser:" + userId);
        // 3. 返回结果
        return new R<>(HttpStatus.HTTP_OK, "退出成功");
    }

    @Override
    public R<User> getUserInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("登录已失效，请重新登录");
        }
        User user = loginUser.getUser();
        return new R<>(HttpStatus.HTTP_OK, user);
    }
}
